package cn.ltgodm.sandbox.service;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.ltgodm.sandbox.enums.ExecuteCodeStatusEnum;
import cn.ltgodm.sandbox.exception.CompliedException;
import cn.ltgodm.sandbox.exception.ExecuteException;
import cn.ltgodm.sandbox.model.dto.*;
import cn.ltgodm.sandbox.utils.LanguageUtil;
import cn.ltgodm.sandbox.utils.ProcessUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallbackTemplate;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StopWatch;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author ltgodm
 * @date 2024-03-10 15:55:39
 * 代码沙箱模板抽象类
 */
@Slf4j
@Data
@ConfigurationProperties(prefix = "sandbox")
public abstract class CodeSandboxTemplate implements CodeSandboxService {

    protected String globalCodePath = "tmpCode";
    protected String className = "Main";
    protected String charset = "UTF-8";
    protected String dockerHost = "";
    protected List<LanguageConfig> languageConfigs;
    final Long[] memory = {0L};
    protected Boolean isMonitoring = false;
    protected Closeable closeable;

    /**
     * 执行代码方法
     *
     * @param executeCodeRequest 请求信息
     * @return 返回响应信息
     */
    @Override
    public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
        try {
            //初始化参数
            List<String> inputList = executeCodeRequest.getInputList();
            String code = executeCodeRequest.getCode();
            String language = executeCodeRequest.getLanguage();

            //1.将用户上传的代码保存到本地
            File userCodeFile = saveUserCodeFile(globalCodePath, className, code);
            String userCodeFilePath = userCodeFile.getParent();

            //2.编译代码，得到编译后的文件
            ExecuteMessage compliedMessage = compliedProcess(userCodeFile.getAbsolutePath());
            if (compliedMessage.getExitValue() != 0) {
                throw new CompliedException(compliedMessage.getErrorMessage());
            }

            //3.获取dockerClient连接
            DockerClient dockerClient = getDockerClient(dockerHost);

            //4.启动容器
            String containerId = createContainer(language, languageConfigs, dockerClient);

            //5.运行代码
            String[] runCode = {"java", "-cp", userCodeFilePath, "Main"};
            List<ExecuteMessage> executeMessageList = runCodeFile(inputList, dockerClient, containerId, runCode);

            //6.清理文件
            clearUserCodeFile(userCodeFile);

            //7.构造返回结果
            ExecuteCodeResponse response = getResponse(executeMessageList);
            return response;
        } catch (Exception e) {
            return ProcessUtil.getErrorResponse(e);
        }
    }

    /**
     * 清理用户上传的代码文件
     *
     * @param userCodeFile 用户代码文件
     */
    protected void clearUserCodeFile(File userCodeFile) {
        //清除文件
        if (userCodeFile.getParentFile() != null) {
            boolean del = FileUtil.del(userCodeFile.getParentFile().getAbsolutePath());
            if (del) {
                log.info("文件清理成功");
            } else {
                log.error("文件清理失败");
            }
        }
    }

    /**
     * 运行代码
     *
     * @param inputList    输入用例
     * @param dockerClient dockerClient连接
     * @param containerId  容器id
     * @return 返回List<ExecuteMessage>集合
     */
    protected List<ExecuteMessage> runCodeFile(List<String> inputList, DockerClient dockerClient, String containerId, String[] runCommand) {
        //创建内存监控
        createMemoryListener(dockerClient, containerId);
        //构造返回结果
        List<ExecuteMessage> executeMessageList = new ArrayList<>();
        for (String inputCase : inputList) {
            ExecuteMessage executeMessage = new ExecuteMessage();
            executeMessage.setTimeout(true);
            StopWatch stopWatch = new StopWatch();
            String[] cases = inputCase.split(" ");
            String[] cmdArray = ArrayUtil.append(runCommand, cases);
            ExecCreateCmd execCreateCmd = dockerClient.execCreateCmd(containerId);
            ExecCreateCmdResponse exec = execCreateCmd
                    .withCmd(cmdArray)
                    .withAttachStdin(true)
                    .withAttachStderr(true)
                    .withAttachStdout(true)
                    .exec();
            log.info("创建执行命令：{}", JSONUtil.toJsonStr(cmdArray));
            String execId = exec.getId();
            ExecStartResultCallback startResultCallback = new ExecStartResultCallback() {
                @Override//执行完成时触发,如果超时则不会触发该方法
                public void onComplete() {
                    executeMessage.setTimeout(false);
                    super.onComplete();
                }

                @Override
                public void onNext(Frame frame) {
                    StreamType streamType = frame.getStreamType();
                    String msg = new String(frame.getPayload());
                    if (StreamType.STDERR.equals(streamType)) {
                        log.info("错误信息：{}", msg);
                        executeMessage.setErrorMessage(msg);
                    } else {
                        log.info("运行信息：{}", StrUtil.cleanBlank(msg));
                        executeMessage.setMessage(StrUtil.cleanBlank(msg));
                    }
                    super.onNext(frame);
                }
            };
            ExecStartCmd execStartCmd = null;
            try {
                //执行命令
                stopWatch.start();
                execStartCmd = dockerClient.execStartCmd(execId);
                execStartCmd.exec(startResultCallback).awaitCompletion(10, TimeUnit.SECONDS);
                log.info("执行命令成功：{}", JSONUtil.toJsonStr(cmdArray));
                stopWatch.stop();
                long millis = stopWatch.getLastTaskTimeMillis();
                executeMessage.setTime(millis);
                executeMessage.setMemory(getMemory());
                log.info("内存占用：{}KB", getMemory() / 1024);
            } catch (InterruptedException e) {
                String msg = "docker执行异常";
                log.error(msg, e);
                executeMessage.setErrorMessage(msg);
                throw new ExecuteException(msg);
            } finally {
                execStartCmd.close();
                execCreateCmd.close();
                executeMessageList.add(executeMessage);
            }
        }
        closeMemoryListener();
        return executeMessageList;
    }

    /**
     * 将代码保存到指定文件
     *
     * @param globalCodePath 保存路径
     * @param className      类名
     * @param code           代码
     * @return 文件
     */
    protected File saveUserCodeFile(String globalCodePath, String className, String code) {
        String userDir = System.getProperty("user.dir");
        String globalCodePathName = userDir + File.separator + globalCodePath;
        if (!FileUtil.exist(globalCodePathName)) {//判断目录是否存在
            FileUtil.mkdir(globalCodePath);
        }
        //把用户的代码隔离存放
        String userCodePath = globalCodePathName + File.separator + UUID.randomUUID();
        String userCodePathName = userCodePath + File.separator + className.concat(".java");
        File userCodeFile = FileUtil.writeString(code, userCodePathName, StandardCharsets.UTF_8);
        return userCodeFile;
    }

    /**
     * 编译代码
     *
     * @param userCodeName 用户上传的代码所在路径
     * @return 返回编译信息
     */
    protected ExecuteMessage compliedProcess(String userCodeName) throws CompliedException {
        //编译文件
        String compliedCMD = StrUtil.format("javac -encoding {} {}", charset, userCodeName);
        log.info("编译命令:{}", compliedCMD);
        try {
            Process compliedProcess = Runtime.getRuntime().exec(compliedCMD);
            ExecuteMessage executeMessage = ProcessUtil.getMessage(compliedProcess, "编译");
            return executeMessage;
        } catch (IOException e) {
            throw new CompliedException("编译错误");
        }
    }

    /**
     * 创建容器
     *
     * @param language        编程语言
     * @param languageConfigs 语言文件配置
     * @param dockerClient    dockerClient连接
     * @return 返回容器id
     */
    protected String createContainer(String language, List<LanguageConfig> languageConfigs, DockerClient dockerClient) {
        //获取根据语言镜像和容器名称
        String imageName = LanguageUtil.getImageName(language, languageConfigs);//获取镜像名称
        String containerName = LanguageUtil.getContainerName(language, languageConfigs);//获取镜像名称
        if (StrUtil.isEmpty(imageName)) {
            throw new ExecuteException("镜像不存在");
        }
        if (StrUtil.isEmpty(containerName)) {
            containerName = language.concat("_code_container");
        }
        //获取docker容器列表
        ListContainersCmd listContainersCmd = dockerClient.listContainersCmd();
        List<Container> containerList = listContainersCmd.withShowAll(true).exec();
        String finalContainerName = containerName;
        Container container = containerList.stream()
                .filter(item -> Arrays.stream(item.getNames())
                        .map(name -> name.replaceFirst("/", ""))
                        .anyMatch(name -> name.equals(finalContainerName)))
                .findFirst().orElse(null);
        String containerId;
        //判断容器是否存在
        if (ObjectUtil.isNotNull(container)) {
            String state = container.getState();
            containerId = container.getId();
            log.info("容器存在,name:{},id:{},state:{}", StrUtil.toString(container.getNames()), container.getId(), state);
            //容器是否运行
            switch (state) {
                case "running":
                    //运行，无须任何操作，返回id
                    return containerId;
                case "paused":
                    //暂停,让容器继续运行
                    UnpauseContainerCmd unpauseContainerCmd = dockerClient.unpauseContainerCmd(containerId);
                    unpauseContainerCmd.exec();
                    unpauseContainerCmd.close();
                    return containerId;
                case "exited":
                    //结束状态
                    RestartContainerCmd restartContainerCmd = dockerClient.restartContainerCmd(containerId);
                    restartContainerCmd.exec();
                    restartContainerCmd.close();
                    return containerId;
            }
        } else {
            //创建容器,检查是否存在镜像
            ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
            List<Image> imageList = listImagesCmd.exec();
            log.info("镜像信息:{}", imageList);
            Image image = imageList.stream().filter(item -> Arrays.asList(item.getRepoTags()).contains(imageName)).findFirst().orElse(null);
            if (ObjectUtil.isNull(image)) {
                //拉取镜像
                pullImage(dockerClient, imageName);
            }
            //创建容器
            CreateContainerCmd createContainerCmd = dockerClient.createContainerCmd(imageName);
            HostConfig hostConfig = HostConfig.newHostConfig();
            hostConfig.setBinds(new Bind("/home/godm/moj/".concat(containerName), new Volume("/".concat(globalCodePath))));
            hostConfig.withMemory(100 * 1024 * 1024L);//限制内存
            hostConfig.withCpuCount(1L);
            CreateContainerResponse createContainerResponse = createContainerCmd
                    .withHostConfig(hostConfig)
                    .withAttachStdin(true)
                    .withAttachStderr(true)
                    .withAttachStdout(true)
                    .withNetworkDisabled(true)//禁止访问网络
                    .withTty(true)
                    .withName(containerName)
                    .exec();
            log.info("创建容器成功：{}", JSONUtil.toJsonStr(createContainerResponse));
            containerId = createContainerResponse.getId();
            //启动容器
            StartContainerCmd startContainerCmd = dockerClient.startContainerCmd(containerId);
            startContainerCmd.exec();

            uploadShellFile(dockerClient, containerId);

            //关闭资源
            listImagesCmd.close();
            startContainerCmd.close();
            createContainerCmd.close();
        }
        //关闭资源
        listContainersCmd.close();
        return containerId;
    }

    /**
     * 拉取镜像
     *
     * @param dockerClient 客户端
     * @param imageName    镜像名称
     */
    protected void pullImage(DockerClient dockerClient, String imageName) {
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(imageName);
        PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() {
            @Override
            public void onNext(PullResponseItem item) {
                log.info("正在拉取{}镜像，当前状态：{},", imageName, item.getStatus());
            }

            @Override
            public void onComplete() {
                log.info("拉取镜像：{} 成功", imageName);
                super.onComplete();
            }
        };
        try {
            pullImageCmd.exec(pullImageResultCallback).awaitCompletion();
        } catch (InterruptedException e) {
            throw new ExecuteException("docker镜像下载失败");
        } finally {
            pullImageCmd.close();
        }
    }

    /**
     * 获取docker连接
     *
     * @param dockerHost 远程docker地址
     * @return 返回dockerClient
     */
    protected DockerClient getDockerClient(String dockerHost) {
        //获取docker连接
        DefaultDockerClientConfig.Builder builder = new DefaultDockerClientConfig.Builder();
        if (StrUtil.isNotBlank(dockerHost)){
            builder.withDockerHost(dockerHost);
        }
        DockerClientConfig dockerClientConfig = builder.build();
        DockerClient dockerClient = DockerClientBuilder.getInstance(dockerClientConfig).build();
        log.info("获取dockerClient连接成功");
        return dockerClient;
    }

    /**
     * 创建容器内存监控
     *
     * @param dockerClient dockerClient连接
     * @param containerId  容器id
     */
    protected StatsCmd createMemoryListener(DockerClient dockerClient, String containerId) {
        ResultCallbackTemplate minitorResultCallback = new ResultCallbackTemplate() {
            @Override
            public void onNext(Object object) {
                if (!isMonitoring) {
                    try {
                        log.info("关闭内存监控");
                        this.close();
                        closeable.close();
                    } catch (IOException e) {
                        throw new ExecuteException("取消内存监控失败:message" + e.getMessage());
                    } finally {
                        isMonitoring = true;
                    }
                }
                Statistics statistics = (Statistics) object;
                Long usage = statistics.getMemoryStats().getUsage();
                memory[0] = usage;
                log.info("当前内存占用：{}", usage);
            }

            @Override
            public void onStart(Closeable stream) {
                isMonitoring = true;
                closeable = stream;
            }

            @Override
            public void onComplete() {
                isMonitoring = false;
            }

            @Override
            public void close() {
                log.info("内存监控已关闭");
                isMonitoring = false;
            }
        };
        if (!isMonitoring) {
            //创建内存监控
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            statsCmd.exec(minitorResultCallback);
            log.info("创建内存监控");
            return statsCmd;
        } else {
            log.info("监控已存在");
            return null;
        }
    }

    /**
     * 关闭内存监控
     */
    protected void closeMemoryListener() {
        if (isMonitoring) {
            isMonitoring = false;
        }
    }


    /**
     * 获取内存信息
     *
     * @return 返回内存信息
     */
    protected Long getMemory() {
        return memory[0];
    }

    /**
     * 构造返回信息
     *
     * @param executeMessageList 执行信息列表
     * @return 返回响应信息
     */
    protected ExecuteCodeResponse getResponse(List<ExecuteMessage> executeMessageList) {
        ExecuteCodeResponse codeResponse = ExecuteCodeResponse.builder().status(ExecuteCodeStatusEnum.EXECUTE_SUCCESS.getValue()).message("运行成功").build();
        //处理返回结果
        List<String> outputList = new ArrayList<>();
        Long maxTime = 0L;
        Long maxMemory = 0L;
        for (ExecuteMessage executeMessage : executeMessageList) {
            //存在错误
            if (StrUtil.isNotEmpty(executeMessage.getErrorMessage())) {
                codeResponse.setMessage(executeMessage.getErrorMessage());
                codeResponse.setStatus(ExecuteCodeStatusEnum.USER_PROGRESS_ERROR.getValue());
                break;
            }
            //是否超时
            if (executeMessage.getTimeout()) {
                codeResponse.setMessage("程序超时");
                codeResponse.setStatus(ExecuteCodeStatusEnum.USER_PROGRESS_ERROR.getValue());
                break;
            }
            //不存在错误
            maxTime = Math.max(maxTime, executeMessage.getTime());
            maxMemory = Math.max(maxMemory, executeMessage.getMemory());
            outputList.add(executeMessage.getMessage());
            codeResponse.setMessage(ExecuteCodeStatusEnum.EXECUTE_SUCCESS.getText());
        }
        codeResponse.setOutputList(outputList);
        JudgeInfo judgeInfo = new JudgeInfo();
        judgeInfo.setMemory(maxMemory);
        judgeInfo.setTime(maxTime);
        codeResponse.setJudgeInfo(judgeInfo);
        return codeResponse;
    }

    /**
     * 上传shell脚本到容器中
     *
     * @param dockerClient dockerClient连接
     * @param containerId  容器id
     */
    protected void uploadShellFile(DockerClient dockerClient, String containerId) {
        // 获取resources目录下的脚本文件
        InputStream saveFileStream = getClass().getResourceAsStream("/shell/saveUserFile.sh");
        InputStream removeFileStream = getClass().getResourceAsStream("/shell/removeUserFile.sh");

        //创建临时文件
        Path saveUserFilePath = Paths.get(System.getProperty("java.io.tmpdir"),"saveUserFile.sh");
        File saveUserFile = saveUserFilePath.toFile();
        Path removeUserfilePath = Paths.get(System.getProperty("java.io.tmpdir"), "removeUserFile.sh");
        File removeUserFile = removeUserfilePath.toFile();

        //将shell脚本上传到docker容器中
        try {
            Files.copy(saveFileStream,saveUserFilePath, StandardCopyOption.REPLACE_EXISTING);
            Files.copy(removeFileStream,removeUserfilePath, StandardCopyOption.REPLACE_EXISTING);
            // 将脚本复制到容器内
            dockerClient.copyArchiveToContainerCmd(containerId).withHostResource(saveUserFile.getAbsolutePath()).withRemotePath(StrUtil.format("/{}/",globalCodePath)).exec();
            dockerClient.copyArchiveToContainerCmd(containerId).withHostResource(removeUserFile.getAbsolutePath()).withRemotePath(StrUtil.format("/{}/",globalCodePath)).exec();
            log.info("shell脚本复制成功");
        } catch (Exception e) {
            log.error("复制shell脚本失败",e);
        }finally {
            try {
                saveFileStream.close();
                removeFileStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            saveUserFile.deleteOnExit();
            removeUserFile.deleteOnExit();
        }
    }

    /**
     * 查询文件是否存在
     *
     * @param path         文件目录
     * @param fileName     文件名称
     * @param dockerClient docker客户端
     * @param containerId  容器id
     * @return 返回结果
     */
    protected boolean selectFileExist(String path, String fileName, DockerClient dockerClient, String containerId) {
        //1.创建执行脚本
                /*  if [ -f /app/saveUserFile.sh ]; then
                        echo "true"
                    else
                        echo "false"
                    fi  */
        String[] cmdArray = new String[]{"if", "[", "-f", StrUtil.format("{}/{}", path, fileName), "];", "then\n", "echo", "\"true\"\n", "else", "echo", "\"false\"\n", "fi"};
        String cmd = StrUtil.toString(cmdArray);
        log.info("查询命令：{}", cmd);
        ExecCreateCmdResponse exec = dockerClient.execCreateCmd(containerId)
                .withCmd(cmdArray)
                .withAttachStdin(true)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .exec();
        String execId = exec.getId();
        final String[] statusMsg = new String[1];
        //2.获取执行信息
        ExecStartResultCallback callback = new ExecStartResultCallback() {
            @Override
            public void onNext(Frame frame) {
                StreamType streamType = frame.getStreamType();
                String msg = new String(frame.getPayload());
                statusMsg[0] = msg;
                if (streamType.equals(StreamType.STDERR)) {
                    //错误
                    log.error("查询出错：{}", msg);
                    throw new ExecuteException("执行查询文件出错：" + msg);
                } else {
                    //成功
                    log.error("查询结果，文件：{}是否存在：{}", fileName, msg);
                }
            }
        };
        //3.执行
        try {
            dockerClient.execStartCmd(execId).exec(callback).awaitCompletion();
        } catch (InterruptedException e) {
            throw new ExecuteException("保存用户代码文件发生异常：" + e.getMessage());
        }
        //4.处理返回结果
        return "true".equals(statusMsg[0]);
    }
}
